#!/usr/bin/env bash
#
# Simple and efficient way to access various statistics in a git repository
################################################################################
# GLOBALS AND SHELL OPTIONS

set -o nounset
set -o errexit

# Beginning git log date. Respects all git datetime formats
_since=${_GIT_SINCE:-}
[[ -n "${_since}" ]] && _since="--since=$_since"

# End of git log date. Respects all git datetime formats
_until=${_GIT_UNTIL:-}
[[ -n "${_until}" ]] && _until="--until=$_until"

# Set files or directories to be excluded in stats
_pathspec=${_GIT_PATHSPEC:-}
[[ -n "${_pathspec}" ]] && _pathspec="-- $_pathspec"

# Set merge commit view strategy. Default is to show no merge commits
# Exclusive shows only merge commits
# Enable shows regular commits together with normal commits
_merges=${_GIT_MERGE_VIEW:-}
_merges=$(echo "$_merges" | awk '{print tolower($0)}')
if [[ "${_merges}" == "exclusive" ]]; then 
    _merges="--merges"
elif [[ "${_merges}" == "enable" ]]; then
    _merges=""
else
    _merges="--no-merges"
fi

# Limit git log output
_limit=${_GIT_LIMIT:-}
if [[ -n "${_limit}" ]]; then 
    _limit=$_limit
else
    _limit=10
fi

# Default menu theme
# Set the legacy theme by typing "export _MENU_THEME=legacy"
_theme="${_MENU_THEME:=default}"

################################################################################
# DESC: Checks to make sure the user has the appropriate utilities installed
# ARGS: None
# OUTS: None
################################################################################
function checkUtils() {
    local -r msg="not found. Please make sure this is installed and in PATH."
    declare -ar utils=("awk" "basename" "cat" "column" "echo" "git" "grep" "head"
        "seq" "sort" "tput" "tr" "uniq" "wc")
    
    for u in "${utils[@]}"
    do
        command -v "$u" >/dev/null 2>&1 || { echo >&2 "$u ${msg}"; exit 1; }
    done
}

################################################################################
# DESC: Help information printed to stdout during non-interactive mode
# ARGS: None
# OUTS: None
################################################################################
function usage() { 
    local -r program=$(basename "$0")

    echo "
NAME
    ${program} - Simple and efficient way to access various stats in a git repo

SYNOPSIS
    For non-interactive mode: ${program} [OPTIONS]
    For interactive mode: ${program}

DESCRIPTION
    Any git repository contains tons of information about commits, contributors,
    and files. Extracting this information is not always trivial, mostly because
    of a gadzillion options to a gadzillion git commands.

    This program allows you to see detailed information about a git repository.

OPTIONS
    -r, --suggest-reviewers
        show the best people to contact to review code
    -T, --detailed-git-stats
        give a detailed list of git stats
    -R, --git-stats-by-branch
        see detailed list of git stats by branch
    -d, --commits-per-day
        displays a list of commits per day
    -m, --commits-by-month
        displays a list of commits per month
    -w, --commits-by-weekday
        displays a list of commits per weekday
    -o, --commits-by-hour
        displays a list of commits per hour
    -A, --commits-by-author-by-hour
        displays a list of commits per hour by author
    -a, --commits-per-author
        displays a list of commits per author
    -S, --my-daily-stats
        see your current daily stats
    -C, --contributors
        see a list of everyone who contributed to the repo
    -b, --branch-tree
        show an ASCII graph of the git repo branch history
    -D, --branches-by-date
        show branches by date
    -c, --changelogs
        see changelogs
    -L, --changelogs-by-author
        see changelogs by author
    -j, --json-output
        save git log as a JSON formatted file to a specified area
    -h, -?, --help
        display this help text in the terminal

ADDITIONAL USAGE
    You can set _GIT_SINCE and _GIT_UNTIL to limit the git time log
        ex: export _GIT_SINCE=\"2017-01-20\"
    You can set _GIT_LIMIT for limited output log
        ex: export _GIT_LIMIT=20
    You can exclude directories or files from the stats by using pathspec
        ex: export _GIT_PATHSPEC=':!pattern'
    You can set _GIT_MERGE_VIEW to view merge commits with normal commits
        ex: export _GIT_MERGE_VIEW=enable
    You can also set _GIT_MERGE_VIEW to only show merge commits
        ex: export _GIT_MERGE_VIEW=exclusive
    You can set _MENU_THEME to display the legacy color scheme
        ex: export _MENU_THEME=legacy"
}

################################################################################
# DESC: Displays the interactive menu and saves the user supplied option
# ARGS: None
# OUTS: $opt: Option selected by the user based on menu choice
################################################################################
function showMenu() {
    local -r normal=$(tput sgr0)
    local -r cyan=$(tput setaf 6)
    local -r bold=$(tput bold)
    local -r red=$(tput setaf 1)
    local -r yellow=$(tput setaf 3)
    local -r white=$(tput setaf 7)
    local titles=""
    local text=""
    local nums=""
    local help_txt=""
    local exit_txt=""

    # Adjustable color menu option
    if [[ "${_theme}" == "legacy" ]]; then
        titles="${bold}${red}" && readonly titles
        text="${normal}${cyan}" && readonly text
        nums="${bold}${yellow}" && readonly nums
        help_txt="${normal}${yellow}" && readonly help_txt
        exit_txt="${bold}${red}" && readonly exit_txt
    else
        titles="${bold}${cyan}" && readonly titles
        text="${normal}${white}" && readonly text
        nums="${normal}${bold}${white}" && readonly nums
        help_txt="${normal}${cyan}" && readonly help_txt
        exit_txt="${bold}${cyan}" && readonly exit_txt
    fi
    
    echo -e "\n${titles} Generate:${normal}"
    echo -e "${nums}    1)${text} Contribution stats (by author)"
    echo -e "${nums}    2)${text} Contribution stats (by author) on a specific branch"
    echo -e "${nums}    3)${text} Git changelogs (last $_limit days)"
    echo -e "${nums}    4)${text} Git changelogs by author"
    echo -e "${nums}    5)${text} My daily status"
    echo -e "${nums}    6)${text} Save git log output in JSON format"
    echo -e "\n${titles} List:"
    echo -e "${nums}    7)${text} Branch tree view (last $_limit)"
    echo -e "${nums}    8)${text} All branches (sorted by most recent commit)"
    echo -e "${nums}    9)${text} All contributors (sorted by name)"
    echo -e "${nums}   10)${text} Git commits per author"
    echo -e "${nums}   11)${text} Git commits per date"
    echo -e "${nums}   12)${text} Git commits per month"
    echo -e "${nums}   13)${text} Git commits per weekday"
    echo -e "${nums}   14)${text} Git commits per hour"
    echo -e "${nums}   15)${text} Git commits by author per hour"
    echo -e "\n${titles} Suggest:"
    echo -e "${nums}   16)${text} Code reviewers (based on git history)"
    echo -e "\n${help_txt}Please enter a menu option or ${exit_txt}press Enter to exit."
    echo -n "${text}> ${normal}"
    read -r opt
}

################################################################################
# DESC: Prints a formatted message of the selected option by the user to stdout
# ARGS: $* (required): String to print (usually provided by other functions)
# OUTS: None
################################################################################
function optionPicked() {
    local -r bold=$(tput bold)
    local -r red=$(tput setaf 1)
    local -r reset=$(tput sgr0)
    local msg=${*:-"${reset}Error: No message passed"}
    
    echo -e "${bold}${red}${msg}${reset}\n"
}

################################################################################
# DESC: Shows detailed contribution stats per author by parsing every commit in
#       the repo and outputting their contribution stats
# ARGS: $branch (optional): Users can specify an alternative branch instead of
#                           the current default one
# OUTS: None
################################################################################
function detailedGitStats() {
    local is_branch_existing=false
    local branch="${1:-}"
    local _branch=""
    
    # Check if requesting for a specific branch
    if [[ -n "${branch}" ]]; then
        # Check if branch exist
        if [[ $(git show-ref refs/heads/"${branch}") ]] ; then
            is_branch_existing=true
            _branch="${branch}"
        else
            is_branch_existing=false
            _branch=""
        fi
    fi

    # Prompt message
    if [[ "${is_branch_existing}" && -n "${_branch}" ]]; then
        optionPicked "Contribution stats (by author) on ${_branch} branch:"
    elif [[ -n "${branch}" && -z "${_branch}" ]]; then
        optionPicked "Branch ${branch} does not exist.\nContribution stats (by author) on the current branch:"
    else
        optionPicked "Contribution stats (by author) on the current branch:"
    fi

    git -c log.showSignature=false log ${_branch} --use-mailmap $_merges --numstat \
        --pretty="format:commit %H%nAuthor: %aN <%aE>%nDate:   %ad%n%n%w(0,4,4)%B%n" \
        $_since $_until $_pathspec | LC_ALL=C awk '
        function printStats(author) {
        printf "\t%s:\n", author

        if(more["total"] > 0) {
            printf "\t  insertions:    %d (%.0f%%)\n", more[author], \
                (more[author] / more["total"] * 100)
        }

        if(less["total"] > 0) {
            printf "\t  deletions:     %d (%.0f%%)\n", less[author], \
                (less[author] / less["total"] * 100)
        }

        if(file["total"] > 0) {
            printf "\t  files:         %d (%.0f%%)\n", file[author], \
                (file[author] / file["total"] * 100)
        }

        if(commits["total"] > 0) {
            printf "\t  commits:       %d (%.0f%%)\n", commits[author], \
                (commits[author] / commits["total"] * 100)
        }

        if (first[author] != "") {
            printf "\t  lines changed: %s\n", more[author] + less[author]
            printf "\t  first commit:  %s\n", first[author]
            printf "\t  last commit:   %s\n", last[author]
        }

        printf "\n"
        }

        /^Author:/ {
        $1 = ""
        author = $0
        commits[author] += 1
        commits["total"] += 1
        }

        /^Date:/ {
        $1="";
        first[author] = substr($0, 2)
        if(last[author] == "" ) { last[author] = first[author] }
        }

        /^[0-9]/ {
        more[author] += $1
        less[author] += $2
        file[author] += 1

        more["total"]  += $1
        less["total"]  += $2
        file["total"]  += 1
        }

        END {
        for (author in commits) {
            if (author != "total") {
            printStats(author)
            }
        }
        printStats("total")
        }'
}

################################################################################
# DESC: Displays the authors in order of total contribution to the repo
# ARGS: None
# OUTS: None
################################################################################
function suggestReviewers() {
    optionPicked "Suggested code reviewers (based on git history):"
    git -c log.showSignature=false log --use-mailmap $_merges $_since $_until \
        --pretty=%aN $_pathspec | head -n 100 | sort | uniq -c | sort -nr | LC_ALL=C awk '
    { args[NR] = $0; }
    END {
      for (i = 1; i <= NR; ++i) {
        printf "%s\n", args[i]
      }
    }' | column -t -s,
}

################################################################################
# DESC: Saves the git log output in a JSON format
# ARGS: $json_path (required): Path to where the file is saved
# OUTS: A JSON formatted file
################################################################################
function jsonOutput() {
    optionPicked "Output log saved to file at: ${json_path:?}/output.json"
    git -c log.showSignature=false log --use-mailmap $_merges $_since $_until \
        --pretty=format:'{%n  "commit": "%H",%n  "abbreviated_commit": "%h",%n  "tree": "%T",%n  "abbreviated_tree": "%t",%n  "parent": "%P",%n  "abbreviated_parent": "%p",%n  "refs": "%D",%n  "encoding": "%e",%n  "subject": "%s",%n  "sanitized_subject_line": "%f",%n  "body": "%b",%n  "commit_notes": "%N",%n  "author": {%n    "name": "%aN",%n    "email": "%aE",%n    "date": "%aD"%n  },%n  "commiter": {%n    "name": "%cN",%n    "email": "%cE",%n    "date": "%cD"%n  }%n},' \
        | sed "$ s/,$//" \
        | sed ':a;N;$!ba;s/\r\n\([^{]\)/\\n\1/g' \
        | awk 'BEGIN { print("[") } { print($0) } END { print("]") }' \
        > "${json_path:?}"/output.json
}

################################################################################
# DESC: Displays a horizontal bar graph based on total commits per month
# ARGS: None
# OUTS: None
################################################################################
function commitsByMonth() {
    optionPicked "Git commits by month:"
    echo -e "\tmonth\tsum"
    for i in Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec
    do
        echo -en "\t$i\t"
        git -c log.showSignature=false shortlog -n $_merges --format='%ad %s' \
            $_since $_until | grep " $i " | wc -l
    done | awk '{ 
        count[$1] = $2 
        total += $2 
    } 
    END{ 
        for (month in count) {
            s="|";
            if (total > 0) {
                percent = ((count[month] / total) * 100) / 1.25;
                for (i = 1; i <= percent; ++i) {
                    s=s"█"
                }
                printf( "\t%s\t%-0s\t%s\n", month, count[month], s );
            }
        }
    }' | LC_TIME="en_EN.UTF-8" sort -M
}

################################################################################
# DESC: Displays a horizontal bar graph based on total commits per weekday
# ARGS: None
# OUTS: None
################################################################################
function commitsByWeekday() {
    optionPicked "Git commits by weekday:"
    echo -e "\tday\tsum"
    local counter=1
    for i in Mon Tue Wed Thu Fri Sat Sun
    do
        echo -en "\t$counter\t$i\t"
        git -c log.showSignature=false shortlog -n $_merges --format='%ad %s' \
            $_since $_until | grep "$i " | wc -l
        counter=$((counter+1)) 
    done | awk '{
    }
    NR == FNR { 
        count[$1" "$2] = $3; 
        total += $3;
        next 
    }
    END{ 
        for (day in count) {
            s="|";
            if (total > 0) {
                percent = ((count[day] / total) * 100) / 1.25;
                for (i = 1; i <= percent; ++i) {
                    s=s"█"
                }
                printf("\t%s\t%s\t%-0s\t%s\n", substr(day,0,1), substr(day,3,5), count[day], s);
            }
        }
    }' | sort -k 1 -n | awk '{$1=""}1' | awk '{$1=$1}1' | awk '{printf("\t%s\t%s\t%s\n", $1, $2, $3)}'
}

################################################################################
# DESC: Displays a horizontal bar graph based on total commits per hour
# ARGS: $author (optional): Can focus on a single author. Default is all authors
# OUTS: None
################################################################################
function commitsByHour() {
    local author="${1:-}"
    local _author=""

    if [[ -z "${author}" ]]; then
        optionPicked "Git commits by hour:"
        _author="--author=**"
    else
        optionPicked "Git commits by hour for author '${author}':"
        _author="--author=${author}"
    fi
    echo -e "\thour\tsum"
    for i in $(seq -w 0 23)
    do
        echo -ne "\t$i\t"
            git -c log.showSignature=false shortlog -n $_merges --format='%ad %s' \
                "${_author}" $_since $_until | grep ' '$i: | wc -l
    done | awk '{ 
        count[$1] = $2 
        total += $2 
    } 
    END{ 
        for (hour in count) {
            s="|";
            if (total > 0) {
                percent = ((count[hour] / total) * 100) / 1.25;
                for (i = 1; i <= percent; ++i) {
                    s=s"█"
                }
                printf( "\t%s\t%-0s\t%s\n", hour, count[hour], s );
            }
        }
    }' | sort
}

################################################################################
# DESC: Shows the number of commits that were committed per date recorded in the
#       repo's log history
# ARGS: None
# OUTS: None
################################################################################
function commitsPerDay() {
    optionPicked "Git commits per date:";
    git -c log.showSignature=false log --use-mailmap $_merges $_since $_until \
        --date=short --format='%ad' $_pathspec | sort | uniq -c
}

################################################################################
# DESC: Displays the number of commits and percentage contributed to the repo
#       per author and sorts them by contribution percentage
# ARGS: None
# OUTS: None
################################################################################
function commitsPerAuthor()  {
    optionPicked "Git commits per author:"
    local authorCommits=$(git -c log.showSignature=false log --use-mailmap $_merges \
      $_since $_until | grep -i Author: | cut -c9-)
    local coAuthorCommits=$(git -c log.showSignature=false log --use-mailmap $_merges \
    $_since $_until | grep -i Co-Authored-by: | cut -c21-)

    if [[ -z "${coAuthorCommits}" ]]
    then
      allCommits="${authorCommits}"
    else
      allCommits="${authorCommits}\n${coAuthorCommits}"
    fi

    echo -e "${allCommits}" | awk '
      { $NF=""; author[NR] = $0 }
      END {
        for(i in author) {
          sum[author[i]]++; name[author[i]] = author[i]; total++;
        }
        for(i in sum) {
          printf "\t%d,%s,%2.1f%%\n", sum[i], name[i], (100 * sum[i] / total)
        }
      }' | sort -n -r | column -t -s,
}

################################################################################
# DESC: Shows git shortstats on the current user's changes for current day
# ARGS: None
# OUTS: None
################################################################################
function myDailyStats() {
    optionPicked "My daily status:"
    git diff --shortstat '@{0 day ago}' | sort -nr | tr ',' '\n' | LC_ALL=C awk '
    { args[NR] = $0; }
    END {
      for (i = 1; i <= NR; ++i) {
        printf "\t%s\n", args[i]
      }
    }'

    echo -e "\t" $(git -c log.showSignature=false log --use-mailmap \
                       --author="$(git config user.name)" $_merges \
                       --since=$(date "+%Y-%m-%dT00:00:00") \
                       --until=$(date "+%Y-%m-%dT23:59:59") --reverse \
                       | grep commit | wc -l) "commits"
}

################################################################################
# DESC: Lists all contributors to a repo sorted by alphabetical order
# ARGS: None
# OUTS: None
################################################################################
function contributors() {
    optionPicked "All contributors (sorted by name):"
    git -c log.showSignature=false log --use-mailmap $_merges $_since $_until \
        --format='%aN' $_pathspec | sort -u | cat -n
}

################################################################################
# DESC: Shows an abbreviated ASCII graph based off of commit history
# ARGS: None
# OUTS: None
################################################################################
function branchTree() {
    optionPicked "Branching tree view:"
    git -c log.showSignature=false log --use-mailmap --graph --abbrev-commit \
        $_since $_until --decorate \
        --format=format:'--+ Commit:  %h %n  | Date:    %aD (%ar) %n''  | Message: %s %d %n''  + Author:  %aN %n' \
        --all | head -n $((_limit*5))
}

################################################################################
# DESC: Lists all branches sorted by their most recent commit
# ARGS: None
# OUTS: None
################################################################################
function branchesByDate() {
    optionPicked "All branches (sorted by most recent commit):"
    git for-each-ref --sort=committerdate refs/heads/ \
        --format='[%(authordate:relative)] %(authorname) %(refname:short)' | cat -n
}

################################################################################
# DESC: Displays the latest commit history in an easy to read format by date
# ARGS: $author (optional): Can focus on a single author. Default is all authors
# OUTS: None
################################################################################
function changelogs() {
    local author="${1:-}"
    local _author=""
    local next=$(date +%F)

    if [[ -z "${author}" ]]; then
        optionPicked "Git changelogs:"
        _author="--author=**"
    else
        optionPicked "Git changelogs for author '${author}':"
        _author="--author=${author}"
    fi

    git -c log.showSignature=false log \
        --use-mailmap \
        $_merges \
        --format="%cd" \
        --date=short "${_author}" $_since $_until $_pathspec \
        | sort -u -r | head -n $_limit \
        | while read DATE; do
              echo -e "\n[$DATE]"
              GIT_PAGER=cat git -c log.showSignature=false log \
                                --use-mailmap $_merges \
                                --format=" * %s (%aN)" "${_author}" \
                                --since=$DATE --until=$next
              next=$DATE
          done
}

################################################################################
# MAIN

# Check to make sure all utilities required for this script are installed
checkUtils

# Check if we are currently in a git repo.
git rev-parse --is-inside-work-tree > /dev/null

# Parse non-interative commands
if [[ "$#" -eq 1 ]]; then
    case "$1" in
        -r|--suggest-reviewers) suggestReviewers;;
        -T|--detailed-git-stats) detailedGitStats;;
        -R|--git-stats-by-branch)
            branch=""
            while [[ -z "${branch}" ]]; do 
                read -r -p "Which branch? " branch
            done
            detailedGitStats "${branch}";;
        -b|--branch-tree) branchTree;;
        -d|--commits-per-day) commitsPerDay;;
        -a|--commits-per-author) commitsPerAuthor;;
        -S|--my-daily-stats) myDailyStats;;
        -C|--contributors) contributors;;
        -D|--branches-by-date) branchesByDate;;
        -c|--changelogs) changelogs;;
        -L|--changelogs-by-author)
            author="${_GIT_AUTHOR:-}"
            while [[ -z "${author}" ]]; do 
                read -r -p "Which author? " author
            done
            changelogs "${author}";;
        -w|--commits-by-weekday) commitsByWeekday;;
        -o|--commits-by-hour) commitsByHour;;
        -A|--commits-by-author-by-hour)
            author="${_GIT_AUTHOR:-}"
            while [[ -z "${author}" ]]; do 
                read -r -p "Which author? " author
            done
            commitsByHour "${author}";;
        -m|--commits-by-month) commitsByMonth;;
        -j|--json-output) 
            json_path=""
            while [[ -z "${json_path}" ]]; do
                read -r -p "Path to save JSON file: " json_path
                if [[ ! -w "${json_path}" ]]; then
                    echo "Invalid path or permission denied to write to given area."
                    json_path=""
                fi
            done
            jsonOutput "${json_path}";;
        -h|-\?|--help) usage;;
        *) echo "Invalid argument"; usage; exit 1;;
    esac
    exit 0;
fi
[[ "$#" -gt 1 ]] && { echo "Invalid arguments"; usage; exit 1; }

# Parse interactive commands
clear
showMenu

while [[ "${opt}" != "" ]]; do
    clear
    case "${opt}" in
        1) detailedGitStats; showMenu;;
        2) branch=""
           while [[ -z "${branch}" ]]; do 
               read -r -p "Which branch? " branch
           done
           detailedGitStats "${branch}"; showMenu;;
        3) changelogs; showMenu;;
        4) author=""
           while [[ -z "${author}" ]]; do 
               read -r -p "Which author? " author
           done
           changelogs "${author}"; showMenu;;
        5) myDailyStats; showMenu;;
        6) json_path=""
           while [[ -z "${json_path}" ]]; do
               read -r -p "Path to save JSON file: " json_path
               if [[ ! -w "${json_path}" ]]; then
                   echo "Invalid path or permission denied to write to given area."
                   json_path=""
               fi
           done
           jsonOutput "${json_path}"; showMenu;;
        7) branchTree; showMenu;;
        8) branchesByDate; showMenu;;
        9) contributors; showMenu;;
       10) commitsPerAuthor; showMenu;;
       11) commitsPerDay; showMenu;;
       12) commitsByMonth; showMenu;;
       13) commitsByWeekday; showMenu;;
       14) commitsByHour; showMenu;;
       15) author=""
           while [[ -z "${author}" ]]; do 
               read -r -p "Which author? " author
           done
           commitsByHour "${author}"; showMenu;;
       16) suggestReviewers; showMenu;;
       q|"\n") exit;;
       *) clear; optionPicked "Pick an option from the menu"; showMenu;;
    esac
done
